Skip to content

[Feat/#116] 행사·사물함·반납 예약 알림 발송 - #121

Open
tnals0924 wants to merge 5 commits into
feat/#115-instant-notificationsfrom
feat/#116-scheduled-notifications
Open

tnals0924 wants to merge 5 commits into
feat/#115-instant-notificationsfrom
feat/#116-scheduled-notifications

Conversation

@tnals0924

Copy link
Copy Markdown
Member

#️⃣연관된 이슈

🎯 해결하려는 문제가 무엇인가요?

정해진 시각에 가는 예약 알림 6종을 보냅니다. 모든 시각은 KST입니다.

알림 스케줄 대상 선정 수신자
신청한 행사 임박 매일 10:00 게시된 행사 중 시작 일시가 내일 신청자 (없으면 기록 안 함)
행사 모집 마감 임박 매일 10:00 게시된 행사 중 신청 마감 일시가 내일이고 지금 모집 중(calculateRecruitStatus == OPEN) 활성 회원 − 신청자
사물함 모집 마감 임박 매일 10:00 게시된 회차 중 신청 마감 일시가 내일이고 신청 기간이 시작됨 활성 회원 − 그 회차 신청자
사물함 이용 기간 만료 임박 매일 10:00 게시된 회차 중 이용 종료일이 3일 뒤 그 회차 신청자 (없으면 기록 안 함)
반납 기한 임박 10분마다 "반납 기한 − 1시간"이 직전 실행 이후 지금까지 사이 대여자
반납 연체 매일 10:00 반납 기한이 지난 대여 중 이력 (반납할 때까지 매일) 대여자

❓ 왜 해결해야 하나요?

#120까지는 사용자 행동 직후 알림만 있고, 시간이 지나서 가야 하는 알림(전날 안내, 마감 임박, 반납 기한)은 보내는 곳이 없었습니다.

⭐ 어떻게 해결했나요?

  • 이벤트 6종 (common.event): EventStartingTomorrow, EventApplyClosingTomorrow, LockerApplyClosingTomorrow, LockerUsageEndingSoon, RentalDueSoon, RentalOverdue. 신청자 기준 알림은 신청자 ID 목록을 싣고, "미신청자" 알림은 신청자 ID를 제외 목록으로 실어 member 쪽이 "활성 회원 − 신청자"를 계산합니다(event 모듈은 회원 전체를 모름).
  • 스케줄러 (각 도메인 service.impl, package-private): 자기 데이터로 대상을 고르고 OutboxWriter로 기록만 합니다. 메서드에 @Transactional(아웃박스 기록은 트랜잭션 필수), cron에 zone = "Asia/Seoul".
    • EventNotificationScheduler (event 모듈)
    • LockerNotificationScheduler (event 모듈)
    • RentalNotificationScheduler (welfare 모듈). 반납 기한은 기존 RentalRecord.dueAt()(물품 반납 정책 기준)으로 계산합니다. 반납 정책이 없는 물품은 기한을 계산할 수 없어 빠집니다.
  • 조회 추가
    • EventRepository: 시작·마감 일시 구간으로 게시 행사 조회, 유효 신청자 ID
    • LockerRepository·LockerApplicationRepository: 마감 일시 구간·이용 종료일로 게시 회차 조회, 회차 신청자 ID
    • RentalHistoryRepository.findAllByStatus, RentalHistoryService.getAllReturnRequiredRentals()(기존 회원별 조회와 같은 짝짓기 로직)
  • 리스너: NotificationEventListener에 6종 메서드를 추가했습니다.
  • 문서: architecture.md 6-2절에 예약 이벤트 스케줄러 위치·규칙을 추가했습니다.
  • 기존 테스트 수정: EventServiceImplTest, LockerServiceImplTest, LockerApplicationServiceImplTest의 가짜 저장소가 포트를 직접 구현해서, 새 메서드에 기존 방식대로 UnsupportedOperationException 스텁을 추가했습니다.

🧩 이 PR의 한계 & 트레이드오프

  • 놓친 회차는 다시 보내지 않음: 발송 기록을 남기지 않고 날짜·시간 구간으로 고릅니다. 서버가 10시나 10분 경계에 내려가 있으면 그 회차 알림은 빠집니다.
  • 반납 기한 임박 구간: 지금 시각을 10분 단위로 내린 시각까지를 봅니다. 스케줄러 스레드가 밀려 실행이 10분 이상 늦어지면 그 사이 구간이 빠질 수 있습니다(스케줄러 풀 크기 기본값 1, 릴레이와 공유).
  • 반납 연체는 하루 한 번 10시: 문구표의 "1시간 경과 시 1회씩" 대신 결정에 따라 매일 10시로 보냅니다.
  • 사물함 모집 기간을 서버가 검사하지 않음: 사물함 신청 API는 apply_end_at을 확인하지 않아, "내일 마감" 알림 뒤에도 신청이 막히지 않습니다(기존 동작, 이번 범위 아님).
  • 대여 중 이력 전체 조회: 10분마다 RENTAL 상태 이력 전체와 물품을 읽습니다. rental_status 인덱스가 없지만 대여 건수가 적어 두지 않았습니다.
  • 로컬 검증 범위: 컴파일, 전체 단위 테스트, ModularityTests·DomainImplAccessTests까지 확인했습니다. 로컬에 DB가 없어 새 JPQL·파생 쿼리와 스케줄 실행은 dev에서 확인해야 합니다(대상 행의 날짜를 내일로 바꾼 뒤 10시 실행 확인, 또는 일시적으로 cron을 바꿔 확인).

⛓️ 기존 기능에 미치는 영향

  • 기존 API는 그대로입니다. 스케줄 작업 3개가 추가되고, 그 시각에 조회 쿼리와 outbox_events INSERT가 생깁니다.
  • RentalHistoryService에 조회 메서드가 하나 늘었습니다.

🔀 Edge Case & 실패 시나리오

상황 동작
내일 시작하는 행사에 신청자 없음 기록 안 함
마감이 내일이지만 강제 마감·정원 마감 기록 안 함
사물함 회차 신청 기간이 아직 시작 전 마감 임박 기록 안 함
반납 정책이 없는 물품 기한 계산 불가로 제외
반납 기한이 이미 지난 뒤 처음 실행 기한 임박은 구간 밖이라 안 보내고, 다음 10시에 연체 알림
스케줄 중 DB 오류 그 회차 트랜잭션 롤백, 기록 없음(다음 회차는 다른 대상)

📋 검토한 대안과 선택 이유

  • 발송 기록 테이블로 정확히 한 번 보장: 놓친 회차도 다시 보낼 수 있지만 테이블과 쿼리가 늘어 이번에는 구간 방식으로 정했습니다.
  • 별도 배치 모듈에서 여러 도메인 조합: 새 모듈과 의존 규칙이 필요해, 각 도메인이 자기 데이터로 대상만 고르고 회원 쪽 계산은 구독 측에 맡겼습니다.
  • 반납 연체 매시간 발송: 야간·주말에 매시간 알림이 가서 하루 한 번으로 정했습니다.

💬 리뷰 포인트

  • 반납 기한 임박의 10분 구간 계산(floorToCheckInterval)
  • 행사·사물함 마감 임박 대상 조건(모집 중, 신청 기간 시작 여부)
  • 이벤트에 신청자 ID 목록을 싣는 방식 (신청자가 많으면 payload가 커짐)

@tnals0924 tnals0924 self-assigned this Oct 14, 2026
@coderabbitai

coderabbitai Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 59 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Repository: billilge/stream-server/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: acff0f66-854d-4c3b-ba32-9d53d676dd9a
📥 Commits

Reviewing files that changed from the base of the PR and between 60cb33b and 50d3cc9.

📒 Files selected for processing: 29
  • core/common/src/main/java/kr/ac/kookmin/stream/common/event/EventApplyClosingTomorrow.java
  • core/common/src/main/java/kr/ac/kookmin/stream/common/event/EventStartingTomorrow.java
  • core/common/src/main/java/kr/ac/kookmin/stream/common/event/LockerApplyClosingTomorrow.java
  • core/common/src/main/java/kr/ac/kookmin/stream/common/event/LockerUsageEndingSoon.java
  • core/common/src/main/java/kr/ac/kookmin/stream/common/event/RentalDueSoon.java
  • core/common/src/main/java/kr/ac/kookmin/stream/common/event/RentalOverdue.java
  • core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/event/repository/EventRepository.java
  • core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/event/service/impl/EventNotificationScheduler.java
  • core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/locker/repository/LockerApplicationRepository.java
  • core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/locker/repository/LockerRepository.java
  • core/domain/event/src/main/java/kr/ac/kookmin/stream/event/domain/locker/service/impl/LockerNotificationScheduler.java
  • core/domain/event/src/test/java/kr/ac/kookmin/stream/event/domain/event/service/impl/EventServiceImplTest.java
  • core/domain/event/src/test/java/kr/ac/kookmin/stream/event/domain/locker/service/impl/LockerApplicationServiceImplTest.java
  • core/domain/event/src/test/java/kr/ac/kookmin/stream/event/domain/locker/service/impl/LockerServiceImplTest.java
  • core/domain/member/src/main/java/kr/ac/kookmin/stream/member/domain/notification/service/impl/NotificationEventListener.java
  • core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/rental/repository/RentalHistoryRepository.java
  • core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/rental/service/RentalHistoryService.java
  • core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/rental/service/impl/RentalHistoryServiceImpl.java
  • core/domain/welfare/src/main/java/kr/ac/kookmin/stream/welfare/domain/rental/service/impl/RentalNotificationScheduler.java
  • docs/conventions/architecture.md
  • infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/EventApplicationJpaRepository.java
  • infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/EventJpaRepository.java
  • infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/EventRepositoryImpl.java
  • infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/LockerApplicationJpaRepository.java
  • infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/LockerApplicationRepositoryImpl.java
  • infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/LockerPeriodJpaRepository.java
  • infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/event/LockerRepositoryImpl.java
  • infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/welfare/RentalHistoryJpaRepository.java
  • infrastructure/db/src/main/java/kr/ac/kookmin/stream/db/welfare/RentalHistoryRepositoryImpl.java
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

행사·사물함·반납 예약 알림 발송

1 participant